package cn.marknum.gateway.config;

import cn.marknum.cas.client.authentication.AuthenticationFilter;
import cn.marknum.cas.client.session.AssertionStorage;
import cn.marknum.cas.client.session.SingleSignOutFilter;
import cn.marknum.cas.client.session.SingleSignOutHandler;
import cn.marknum.cas.client.util.FilterConfig;
import cn.marknum.cas.client.validation.Assertion;
import cn.marknum.cas.client.validation.Cas10TicketValidationFilter;
import cn.marknum.feign.clients.TokenFeginClient;
import cn.marknum.gateway.cas.NothingHostnameVerifier;
import cn.marknum.gateway.cas.RedisAuthorizationMappingStorage;
import cn.marknum.gateway.filter.AppRedirectFilter;
import cn.marknum.gateway.filter.AuthUtil;
import cn.marknum.gateway.filter.CasOpenStateFilter;
import cn.marknum.gateway.filter.GenerateTokenAfterCasLoginFilter;
import com.alibaba.fastjson2.JSONWriter;
import com.alibaba.fastjson2.support.config.FastJsonConfig;
import com.alibaba.fastjson2.support.spring.http.converter.FastJsonHttpMessageConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.cors.reactive.CorsUtils;
import org.springframework.web.server.WebFilter;
import reactor.core.publisher.Mono;

import javax.annotation.Resource;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.time.Duration;

/**
 * CAS单点登录配置
 *
 * @author zhangzhao    2021-08-17 15:05
 */
@Configuration
public class MyFilterConfig {
    private Logger logger = LoggerFactory.getLogger(getClass());
    @Resource
    private TokenFeginClient tokenFeginClient;

    @Bean
    @ConditionalOnProperty(prefix = "cas", name = "open", havingValue = "true")
    public WebFilter corsFilter() {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            ServerHttpResponse response = exchange.getResponse();
            if (CorsUtils.isCorsRequest(request)) {
                response.getHeaders().add("Access-Control-Allow-Credentials", "true");
                response.getHeaders().add("Access-Control-Allow-Origin", request.getHeaders().getFirst("Origin"));
                response.getHeaders().add("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT");
                response.getHeaders().add("Access-Control-Allow-Headers", "Content-Type,X-CAF-Authorization-Token,sessionToken,X-TOKEN");
                if (request.getMethod() == HttpMethod.OPTIONS) {
                    response.setStatusCode(HttpStatus.OK);
                    return Mono.empty();
                }
            }
            return chain.filter(exchange);
        };
    }

    @Resource
    private AuthUtil authUtil;

    //仅延安使用
    @Bean
    public WebFilter appRedirectFilter() {
        return new AppRedirectFilter();
    }

    @Bean
    public WebFilter casStateFilter(@Value("${error-return-html-urls:#{null}}") String errorToHtmlUrls, CasProperties casProperties, AssertionStorage assertionStorage) {
        return new CasOpenStateFilter(errorToHtmlUrls, casProperties.server.logoutUrl, assertionStorage);
    }

    @Bean
    @ConditionalOnProperty(prefix = "cas", name = "open", havingValue = "true")
    public SingleSignOutHandler singleSignOutHandler(AssertionStorage assertionStorage, TokenFeginClient tokenFeginClient) {
        return new SingleSignOutHandler() {
            @Override
            protected void doAuthorizationExpire(String authorization) {
                tokenFeginClient.logout(authorization);
                assertionStorage.delete(authorization);
            }
        };
    }

    @Bean
    public HttpMessageConverters messageConverters() {
        FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
        fastJsonHttpMessageConverter.setDefaultCharset(StandardCharsets.UTF_8);
        FastJsonConfig fastJsonConfig = new FastJsonConfig();
        fastJsonConfig.setWriterFeatures(JSONWriter.Feature.WriteNullBooleanAsFalse);
        fastJsonHttpMessageConverter.setFastJsonConfig(fastJsonConfig);
        return new HttpMessageConverters(fastJsonHttpMessageConverter);
    }

    @Bean
    @ConditionalOnProperty(prefix = "cas", name = "open", havingValue = "true")
    public GlobalFilter casLogoutFilter(CasProperties casProperties, SingleSignOutHandler singleSignOutHandler, StringRedisTemplate redisTemplate) {
        FilterConfig filterConfig = new FilterConfig();
        filterConfig.addInitParameter("casServerUrlPrefix", casProperties.server.prefix);
        filterConfig.addInitParameter("secondsCleanExpiredSessions", "3600");
        SingleSignOutFilter.setSingleSignOutHandler(singleSignOutHandler);
        SingleSignOutFilter filter = new SingleSignOutFilter(filterConfig);
        filter.setSessionMappingStorage(new RedisAuthorizationMappingStorage(redisTemplate));
        filter.setOrder(-9);
        return filter;
    }

    @Bean
    @ConditionalOnProperty(prefix = "cas", name = "open", havingValue = "true")
    public GlobalFilter authenticationFilter(CasProperties casProperties, AssertionStorage assertionStorage) {
        disableSslVerification();
        FilterConfig filterConfig = new FilterConfig();
        filterConfig.addInitParameter("casServerLoginUrl", casProperties.server.loginUrl);
        filterConfig.addInitParameter("serverName", casProperties.client.prefix);
        filterConfig.addInitParameter("ignorePattern", "(getVars|testCheck|image|license|error|login|logout|index|heartbeat|yzm|xtpz/cx|nio/v1|web/socket|/tymh/.*|/lingyi-.*)");
        AuthenticationFilter filter = new AuthenticationFilter(filterConfig);
        filter.setRequestUrlHandler(url -> authUtil.filtration(url));
        filter.setAssertionStorage(assertionStorage);
        filter.setOrder(-9);
        return filter;
    }

    @Bean
    @ConditionalOnProperty(prefix = "cas", name = "open", havingValue = "true")
    public AssertionStorage assertionStorage(@Qualifier("objectRedisTemplate") RedisTemplate<String, Serializable> objectRedisTemplate) {
        return new AssertionStorage() {
            private String prefix = "assertion:";

            @Override
            public Assertion get(String key) {
                if (key == null) return null;
                return (Assertion) objectRedisTemplate.opsForValue().get(prefix + key);
            }

            @Override
            public void put(String key, Assertion assertion) {
                if (key == null) return;
                objectRedisTemplate.opsForValue().set(prefix + key, assertion);
                objectRedisTemplate.expire(prefix + key, Duration.ofHours(8));
            }

            @Override
            public void delete(String key) {
                if (key == null) return;
                objectRedisTemplate.delete(prefix + key);
            }
        };
    }

    @Bean
    @ConditionalOnProperty(prefix = "cas", name = "open", havingValue = "true")
    public GlobalFilter cas10TicketValidationFilter(CasProperties casProperties) {
        FilterConfig filterConfig = new FilterConfig();
        filterConfig.addInitParameter("casServerUrlPrefix", casProperties.server.prefix);
        filterConfig.addInitParameter("serverName", casProperties.client.prefix);
        filterConfig.addInitParameter("hostnameVerifier", NothingHostnameVerifier.class.getName());
        Cas10TicketValidationFilter filter = new Cas10TicketValidationFilter(filterConfig);
        filter.setOrder(-8);
        return filter;
    }

    private static void disableSslVerification() {
        try {
            TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {
                public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                    return null;
                }

                public void checkClientTrusted(X509Certificate[] certs, String authType) {
                }

                public void checkServerTrusted(X509Certificate[] certs, String authType) {
                }
            }};
            SSLContext sc = SSLContext.getInstance("SSL");
            sc.init(null, trustAllCerts, new java.security.SecureRandom());
            HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (KeyManagementException e) {
            e.printStackTrace();
        }
    }

    @Bean
    @ConditionalOnProperty(prefix = "cas", name = "open", havingValue = "true")
    public GlobalFilter generateTokenAfterCasLoginFilter(CasProperties casProperties, AssertionStorage assertionStorage) {
        return new GenerateTokenAfterCasLoginFilter(tokenFeginClient, -5, casProperties.server.logoutUrl, assertionStorage, authUtil);
    }
}
